Skip to content

feat(data): entities_fts triggers with cascading refresh#961

Merged
cpcloud merged 1 commit intomicasa-dev:mainfrom
cpcloud:fts-triggers
Apr 20, 2026
Merged

feat(data): entities_fts triggers with cascading refresh#961
cpcloud merged 1 commit intomicasa-dev:mainfrom
cpcloud:fts-triggers

Conversation

@cpcloud
Copy link
Copy Markdown
Collaborator

@cpcloud cpcloud commented Apr 20, 2026

Summary

Installs AFTER INSERT / UPDATE / DELETE triggers on every source table that contributes to entities_fts (projects, vendors, appliances, maintenance_items, incidents, service_log_entries, quotes) so the index stays current without RebuildFTSIndex on every app open.

  • Parent tables whose text is embedded in a child's entity_name (project.title and vendor.name in quote, maintenance_item.name in service_log) get companion _au_cascade triggers that rebuild the child's FTS row when the parent is updated.
  • Cascade JOINs filter on parent.deleted_at IS NULL so a parent soft-delete degrades the child's entity_name (project title disappears from the quote; vendor name disappears; SLE name blanks out) instead of leaving stale text in the index.
  • The populate path carries the same filter so initial rebuilds match the trigger invariant.
  • Trigger installation is idempotent (DROP IF EXISTS + CREATE), so schema drift heals on the next Store.Open. FK constraints (RESTRICT on quote parents, CASCADE on SLE parents) keep the trigger semantics consistent with the rest of the domain.

Stacked on top of #960 (FTS engine). Diff will shrink to just the trigger additions once that merges.

Refs #707

setupEntitiesFTS now installs AFTER INSERT / UPDATE / DELETE triggers
on every source table that contributes rows to entities_fts (projects,
vendors, appliances, maintenance_items, incidents,
service_log_entries, quotes). Parent tables whose text is embedded in
a child's entity_name (project.title and vendor.name in quote,
maintenance_item.name in SLE) get companion _au_cascade triggers that
rebuild the child's FTS row when the parent is updated.

Cascade JOINs filter on parent.deleted_at IS NULL so a parent
soft-delete degrades the child's entity_name (project title
disappears from the quote; vendor name disappears; SLE name blanks
out) instead of leaving stale text in the index. The populate path
carries the same filter so initial rebuilds on app open match the
trigger invariant.

Trigger installation is idempotent (DROP IF EXISTS + CREATE), so
schema drift across app versions heals on the next Store.Open. FK
constraints (RESTRICT on quote parents, CASCADE on SLE parents)
continue to govern hard-delete feasibility; parent _ad triggers are
plain single-table cleanups, no cascade blocks needed.

Tests cover: insert, rename, soft-delete, parent-rename cascade for
all three relationships, parent-soft-delete cascade via raw DML (the
app gates soft-delete with live children, so the cascade path is
exercised by sync in production; raw DML matches that scenario in
tests), FK cascade on maintenance_item hard-delete, and initial
rebuild preserving the soft-delete filter for both SLE and quote
joins.

Refs micasa-dev#707.
@cpcloud cpcloud enabled auto-merge (squash) April 20, 2026 14:48
@cpcloud cpcloud merged commit c5f3f84 into micasa-dev:main Apr 20, 2026
28 checks passed
cpcloud added a commit that referenced this pull request Apr 21, 2026
…962)

## Summary

Hardens `SearchEntities` against real-world natural-language queries and
against single-type result floods.

**Ranking**: three-tier window-function query replaces the flat `LIMIT
20`:

- Tier 1 takes exactly one row per matching entity type (guarantees
cross-type representation).
- Tier 2 raises each type up to `ftsEntityKPerType` rows so single noisy
types can't dominate.
- Tier 3 fills the remaining room up to `ftsEntityTotalCap` from
whatever's left, globally ranked. Single-type searches use the full cap
this way.

Package-level tuning constants (not user-configurable — the eval harness
is the tuning channel):

    ftsEntityKPerType    = 5
    ftsEntityRankCeiling = 0.0   // permissive; eval will tighten
    ftsEntityTotalCap    = 20

`entity_id` tiebreaks rank in every `ORDER BY` so results are stable
when BM25 produces identical ranks.

**Query tolerance**:

- `prepareFTSEntityQuery` lowercases, strips non-alphanum, drops short
and stopword tokens, and OR-joins the survivors as quoted prefix
phrases.
- Returns early when no content words survive so a pure-stopword
question like "what is it?" doesn't hammer FTS with an empty MATCH.

Stacked on top of #961 (triggers), which is stacked on #960 (engine).

Refs #707
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data Data layer, models, database enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant